package filewriter

import (
	"context"
	"fmt"
	"os"
	"sort"
	"sync/atomic"
	"time"

	"gitee.com/simonxie979/skymeta/logutil/port"
)

const (
	MaxBakFileNum = 32
	MaxFileSize   = 1 << 29
	TimeFormat    = "2006_01_02_15_04_05"
)

// BakWriter The main functions are:
//  1. Automatically cuts files based on the set file size.
//  2. Automatically deletes redundant backup files based on
//     the specified number of backup files.
//  3. Write data to file with asynchronization.
type BakWriter struct {
	ctx context.Context

	filePath string      // file path
	fileName string      // file name
	dataChan chan []byte // data channel
	doneFlag int32       // write finish flag
}

func NewBakWriter(ctx context.Context, path, name string) port.FileWriter {
	w := new(BakWriter)
	w.ctx = ctx
	w.filePath = path
	w.fileName = name
	w.dataChan = make(chan []byte, 10000)

	return w
}

// getBackupPath Get path of backup directory
func (w *BakWriter) getBackupPath() string {
	return w.filePath + "/backup"
}

func (w *BakWriter) getFileFullName() string {
	return w.filePath + "/" + w.fileName + ".log"
}

func (w *BakWriter) getBackupFileFullName() string {
	return fmt.Sprintf("%s/%s.%s.log", w.getBackupPath(), w.fileName, time.Now().Format(TimeFormat))
}

func (w *BakWriter) backupFileDelete() {
	backupPath := w.getBackupPath()

	directory, err := os.Open(backupPath)
	if err != nil {
		fmt.Fprintf(os.Stderr, "BakWriter open backup directory %s failure. err: %s", backupPath, err)
		return
	}

	fileList, err := directory.Readdir(0)
	if err != nil {
		fmt.Fprintf(os.Stderr, "BakWriter read file list in backup directory %s failure. err: %s", backupPath, err)
		return
	}

	bakList := NewFileList(w.fileName, fileList)
	if bakList.Len() <= MaxBakFileNum {
		return
	}

	sort.Sort(bakList)

	for i := MaxBakFileNum; i < bakList.Len(); i++ {
		os.Remove(backupPath + "/" + bakList[i].Name())
	}
}

func (w *BakWriter) openFile() *os.File {
	if err := os.MkdirAll(w.filePath, os.ModePerm); err != nil {
		fmt.Fprintf(os.Stderr, "BakWriter make dir %s failure. err: %s", w.filePath, err)
		return nil
	}

	fullName := w.getFileFullName()
	file, err := os.OpenFile(fullName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
	if err != nil {
		fmt.Fprintf(os.Stderr, "BakWriter open file %s failure. err: %s", fullName, err)
		return nil
	}

	fileInfo, err := file.Stat()
	if err != nil {
		fmt.Fprintf(os.Stderr, "BakWriter get file[%s] info failure. err: %s", fullName, err)
		return nil
	}

	if fileInfo.Size() < MaxFileSize {
		return file
	}
	// close old file handle
	file.Close()

	bakPath := w.getBackupPath()
	if err := os.MkdirAll(bakPath, os.ModePerm); err != nil {
		fmt.Fprintf(os.Stderr, "BakWriter make log back dir %s failure. err: %s", bakPath, err)
		return nil
	}

	bakFullName := w.getBackupFileFullName()
	if err := os.Rename(fullName, bakFullName); err != nil {
		fmt.Fprintf(os.Stderr, "BakWriter rename %s to %s failure. err: %s\n", fullName, bakFullName, err)
		return nil
	}

	w.backupFileDelete()

	file, err = os.OpenFile(fullName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
	if err != nil {
		fmt.Fprintf(os.Stderr, "BakWriter reopen file %s failure. err: %s", fullName, err)
		return nil
	}

	return file
}

func (w *BakWriter) doWrite(buf []byte) {
	file := w.openFile()
	if file == nil {
		return
	}
	defer file.Close()

	if _, err := file.Write(buf); err != nil {
		fmt.Fprintf(os.Stderr, "BakWriter write file failed. err: %s\n", err)
		return
	}

	if err := file.Sync(); err != nil {
		fmt.Fprintf(os.Stderr, "BakWriter sync file failed. err: %s\n", err)
	}
}

func (w *BakWriter) asyncWrite() {
	var buf []byte
	for {
		select {
		case data := <-w.dataChan:
			buf = append(buf, data...)
			for i := 0; i < len(w.dataChan); i++ {
				buf = append(buf, <-w.dataChan...)
			}
			w.doWrite(buf)
		case <-w.ctx.Done():
			for len(w.dataChan) > 0 {
				for i := 0; i < len(w.dataChan); i++ {
					buf = append(buf, <-w.dataChan...)
				}
				time.Sleep(time.Millisecond)
			}
			w.doWrite(buf)
			atomic.CompareAndSwapInt32(&w.doneFlag, 0, 1)
			return
		}
		buf = buf[:0]
	}
}

// Serve Launch a goroutine for asynchronization write data to file.
func (w *BakWriter) Serve() {
	go w.asyncWrite()
}

// Write Input data to in pipe.
// This opreation may block current goroutine
func (w *BakWriter) Write(data []byte) {
	if atomic.LoadInt32(&w.doneFlag) == 0 {
		w.dataChan <- data
	}
}

// Done Returns the log data output status.
// If true is retured, indicates that all log data has been written to a file.
func (w *BakWriter) Done() bool {
	if atomic.LoadInt32(&w.doneFlag) == 1 {
		close(w.dataChan)
		return true
	} else {
		return false
	}
}
